
/*
 * Copyright (C) 2008-2009 Archie L. Cobbs. All rights reserved.
 *
 * $Id: SchemaModel.java 310 2010-02-05 21:20:06Z archie.cobbs $
 */

package org.dellroad.sidekar.schema.model;

import java.io.PrintStream;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

import org.dellroad.sidekar.schema.SchemaMismatchException;
import org.dellroad.sidekar.schema.change.AddEntityChange;
import org.dellroad.sidekar.schema.change.AddFieldChange;
import org.dellroad.sidekar.schema.change.AddIndexChange;
import org.dellroad.sidekar.schema.change.EntityAbstractnessChange;
import org.dellroad.sidekar.schema.change.RemoveEntityChange;
import org.dellroad.sidekar.schema.change.RemoveFieldChange;
import org.dellroad.sidekar.schema.change.RemoveIndexChange;
import org.dellroad.sidekar.schema.change.RenameEntityChange;
import org.dellroad.sidekar.schema.change.SchemaChangeSwitch;

/**
 * A Sidekar schema modeled in Java objects.
 */
public class SchemaModel implements Cloneable, SchemaChangeSwitch {

    private final TreeMap<String, EntityModel> entityModels = new TreeMap<String, EntityModel>();

    /**
     * Default constructor.
     */
    public SchemaModel() {
    }

    /**
     * Copy constructor. Performs a deep-copy so that this instance and the original share no common objects.
     */
    public SchemaModel(SchemaModel original) {
        for (Map.Entry<String, EntityModel> entry : original.entityModels.entrySet())
            this.entityModels.put(entry.getKey(), new EntityModel(entry.getValue(), original));
    }

    /**
     * Get all {@link EntityModel}s in this instance, indexed by name.
     *
     * @return mapping from entity name to entity model
     */
    public Map<String, EntityModel> getEntityModels() {
        return this.entityModels;
    }

    /**
     * Reset this instance so that it is empty.
     */
    public void clear() {
        this.getEntityModels().clear();
    }

    // Used by JiBX
    @SuppressWarnings("unused")
    private void addEntityModel(EntityModel entityModel) {
        if (this.entityModels.containsKey(entityModel.getName()))
            throw new IllegalArgumentException("entity `" + entityModel.getName() + "' already exists");
        this.entityModels.put(entityModel.getName(), entityModel);
    }

    // Used by JiBX
    @SuppressWarnings("unused")
    private Iterator<EntityModel> iterateEntityModels() {
        return this.entityModels.values().iterator();
    }

    /**
     * Validate this schema.
     *
     * @throws IllegalArgumentException if schema is invalid
     */
    public void validate() {
        for (EntityModel entityModel : this.entityModels.values()) {
            try {
                entityModel.validate(this);
            } catch (IllegalArgumentException e) {
                throw new IllegalArgumentException("invalid " + entityModel + ": " + e.getMessage(), e);
            }
        }
    }

    /**
     * Print a description of this schema.
     */
    public void print(PrintStream ps) {
        ps.println("schema {");
        for (EntityModel entityModel : this.entityModels.values())
            entityModel.print(ps);
        ps.println("}");
    }

    /**
     * Determine if the given schema has the same structure as this one. In other words, a schema update should be required
     * if and only if this returns {@code false}.
     *
     * @throws SchemaMismatchException if types are different
     */
    public void checkSameSchema(final SchemaModel that) {
        CheckPair.runWithChecks(new Runnable() {
            public void run() {
                SchemaModel.this.doCheckSameSchema(that);
            }
        });
    }

// Schema changes

    /**
     * Apply the given schema change to this model.
     *
     * @throws IllegalArgumentException if the change is not valid with respect to this model 
     */
    @Override
    public void caseAddEntityChange(AddEntityChange change) {
        
        // Validate
        change.checkValidFor(this);

        // Find parent
        EntityModel parentModel = change.getParentName() != null ? this.entityModels.get(change.getParentName()) : null;

        // Add entity
        EntityModel entityModel = new EntityModel();
        entityModel.setName(change.getEntityName());
        entityModel.setParentModel(parentModel);
        entityModel.setAbstract(change.getAbstractness());
        this.entityModels.put(change.getEntityName(), entityModel);
    }

    @Override
    public void caseAddFieldChange(AddFieldChange change) {
        
        // Validate
        change.checkValidFor(this);

        // Add field
        FieldModel fieldModel = FieldDeepCopier.copy(change.getFieldModel(), this);
        EntityModel entityModel = this.entityModels.get(change.getEntityName());
        entityModel.getFieldModels().put(fieldModel.getName(), fieldModel);
    }

    @Override
    public void caseAddIndexChange(AddIndexChange change) {
        
        // Validate
        change.checkValidFor(this);

        // Add index
        EntityModel entityModel = this.entityModels.get(change.getEntityName());
        IndexModel indexModel = new IndexModel(change.getIndexModel());
        entityModel.getIndexModels().put(indexModel.getName(), indexModel);
    }

    @Override
    public void caseEntityAbstractnessChange(EntityAbstractnessChange change) {
        
        // Validate
        change.checkValidFor(this);

        // Update entity
        EntityModel entityModel = this.entityModels.get(change.getEntityName());
        entityModel.setAbstract(change.getAbstractness());
    }

    @Override
    public void caseRemoveEntityChange(RemoveEntityChange change) {
        
        // Validate
        change.checkValidFor(this);

        // Remove entity
        EntityModel entityModel = this.entityModels.get(change.getEntityName());
        this.entityModels.remove(entityModel.getName());
    }

    @Override
    public void caseRemoveFieldChange(RemoveFieldChange change) {
        
        // Validate
        change.checkValidFor(this);

        // Remove field
        EntityModel entityModel = this.entityModels.get(change.getEntityName());
        entityModel.getFieldModels().remove(change.getFieldName());
    }

    @Override
    public void caseRemoveIndexChange(RemoveIndexChange change) {
        
        // Validate
        change.checkValidFor(this);

        // Remove index
        EntityModel entityModel = this.entityModels.get(change.getEntityName());
        entityModel.getIndexModels().remove(change.getIndexName());
    }

    @Override
    public void caseRenameEntityChange(RenameEntityChange change) {
        
        // Validate
        change.checkValidFor(this);

        // Rename entity
        EntityModel entityModel = this.entityModels.remove(change.getEntityName());
        entityModel.setName(change.getNewEntityName());
        this.entityModels.put(entityModel.getName(), entityModel);
    }

    private void doCheckSameSchema(SchemaModel that) {

        // Verify all the same entities exist in each schema
        for (String name : this.entityModels.keySet()) {
            if (!that.entityModels.containsKey(name))
                throw new SchemaMismatchException(this, that, "entity `" + name + "' is missing");
        }
        for (String name : that.entityModels.keySet()) {
            if (!this.entityModels.containsKey(name))
                throw new SchemaMismatchException(this, that, "entity `" + name + "' is extra");
        }

        // Compare corresponding entities having the same name
        for (Map.Entry<String, EntityModel> entry : this.entityModels.entrySet()) {
            String name = entry.getKey();
            EntityModel thisEntity = entry.getValue();
            EntityModel thatEntity = that.entityModels.get(name);
            try {
                thisEntity.checkSameEntity(thatEntity);
            } catch (SchemaMismatchException e) {
                throw new SchemaMismatchException("entity `" + name + "' differs: " + e.getMessage(), e);
            }
        }
    }
}

